import os
import pickle  #对象序列化
import numpy as np
import platform

#padding或者截取，sequence是index序列
def pad_and_truncate(sequence, maxlen, dtype='int64', padding='post', truncating='post', value=0):
    x = (np.ones(maxlen) * value).astype(dtype)  #建立最大长度的字符数组，多的补value，value默认补零
    if truncating == 'pre':  #取截取的部分
        trunc = sequence[-maxlen:]  #从后往前截取
    else:
        trunc = sequence[:maxlen]  #从前往后截取，如果超出了maxlen就取maxlen个字，每个字在这里已经是index了
    trunc = np.asarray(trunc, dtype=dtype)  #转换为字符串数组

    if padding == 'post':  #原始的x已经是1了，这时只是将实际的文本放入其中，相当于先padding再填充实际的数据
        x[:len(trunc)] = trunc #将从后往前截取的部分作为返回值
    else:  #反向是用于双向的网络结构
        x[-len(trunc):] = trunc #将从前往后截取x的部分作为返回值
    return x

def build_tokenizer(fnames, max_seq_len, dat_fname):  #读取文件中的文本内容，利用文本内容去构建字典
    if os.path.exists(dat_fname):  #如果存在已经建立好的字典
        print('loading tokenizer:', dat_fname)
        tokenizer = pickle.load(open(dat_fname, 'rb'))  #从磁盘上加载文件，创建对象
    else:
        text = ''  #字符串
        for fname in fnames:  #训练集和测试集文件
            fin = open(fname, 'r', encoding='utf-8', newline='\n', errors='ignore')  #打开训练集/测试集文件
            lines = fin.readlines()  #一次性读入文件
            fin.close()
            for i in range(0, len(lines), 3):
                #查找第i行中包含$T$的位置，得到该模式串左侧的字符串和右侧的字符串，并且将方面词去掉空格？
                text_left, _, text_right = [s.lower().strip() for s in lines[i].partition("$T$")]
                aspect = lines[i + 1].lower().strip()  #得到方面词字符串
                text_raw = text_left + " " + aspect + " " + text_right  #得到实际的句子
                text += text_raw + " "  #直接在读入的上一个句子之后加个空格作为分割

        tokenizer = Tokenizer(max_seq_len)  #传入opt中的最大序列长度
        tokenizer.fit_on_text(text)  #建立字典，将读入的所有文字输入到tokenizer中，逐个转换为index
        pickle.dump(tokenizer, open(dat_fname, 'wb'))  #对象实例化
    return tokenizer

#path='./glove.42B.300d.txt',载入词向量，要手动下载放在文件夹里面，固定词向量，Bert预训练就不用了
def _load_word_vec(path, word2idx=None, embed_dim=300):  #默认不使用one-hot,300维度
    '''
        建立{单词：vec}的字典，这里要花费较多时间
        glove的每一行：单词，单词对应的表示
        
        1、先读取一行;
        2、分离出单词和词向量
        3、注意：使用这种方法的前提是词表顺序和glove.txt文件里的单词顺序一致。
    '''
    fin = open(path, 'r', encoding='utf-8', newline='\n', errors='ignore')  #打开预训练好的词向量文件
    word_vec = {}  #词向量列表
    for line in fin:
        tokens = line.rstrip().split()  #token[0]为单词，之后的为对应词向量。rstrip() 删除 string 字符串末尾的指定字符，为空白字符（包括回车），然后按空格分解，生成一个300维的列表？
        #从列表的开头到倒数第300个元素[:-embed_dim]，然后让短语通过空格组合起来，同理vec是从倒数第300个一直到最后（问题：真的有短语吗）
        word, vec = ' '.join(tokens[:-embed_dim]), tokens[-embed_dim:]
        if word in word2idx.keys():
            word_vec[word] = np.asarray(vec, dtype='float32')  #建立起词和对应向量的字典
    return word_vec


'''
    #建立词嵌入矩阵，根据{单词:vec}字典得到{index:vec}矩阵
    tokenizer.word2idx提供第一个参数
    embed_dim由train.py的opt给出
    dat_fname='{0}_{1}_embedding_matrix.dat'.format(str(opt.embed_dim), opt.dataset))
    
    得到的词嵌入矩阵传入到网络中
    build_embedding_matrix（建立{index：vec}字典->load_word_vec(建立{word：vec}字典）
    执行模型的时候ABSADataset的输入是tokenizer处理之后的index序列，这个时候就可以根据矩阵得到对应的vec了
    建立好matrix，就可以调用nn.Embedding.fromPreTrained了
'''
def build_embedding_matrix(word2idx, embed_dim, dat_fname):
    if os.path.exists(dat_fname):  #如果存在dat文件，也就是之前已经训练好的词嵌入矩阵，大小为(len(word2idx) + 2，embedding_dim)
        print('loading embedding_matrix:', dat_fname)
        embedding_matrix = pickle.load(open(dat_fname, 'rb'))  #从文件中读取并放入到内存中
    else:
        print('loading word vectors...')  #载入词向量，不存在现成词嵌入矩阵的时候run的时候跑到这儿
        embedding_matrix = np.zeros((len(word2idx) + 2, embed_dim))  # idx 0 and len(word2idx)+1 are all-zeros，axis=0是词表长度，axis=1是词向量长度

        #读入文件，如果设置的不是300维，就设置不同的名字
        if platform.system().lower() == 'windows':  #针对不同平台进行设置
            fname = './glove.twitter.27B/glove.twitter.27B.' + str(embed_dim) + 'd.txt' \
                if embed_dim != 300 else 'D:\\thesis\\code\\ABSA-PyTorch-master\\glove.42B.300d.txt'  #这里的词向量文件固定好了
        else:
            print('*'*50,'importing glove','*'*50)
            fname ='../glove.42B.300d.txt'  #在文字所在的当前目录下的文件名，应该是../才对？

        word_vec = _load_word_vec(fname, word2idx=word2idx, embed_dim=embed_dim)  #得到字典{词语：词向量}
        print('building embedding_matrix:', dat_fname)
        for word, i in word2idx.items():  #逐个读入词和对应的index
            vec = word_vec.get(word)  #word_vec：dict，调用get方法
            if vec is not None:
                # words not found in embedding index will be all-zeros.
                embedding_matrix[i] = vec  #矩阵中的第i个0行赋值为得到的词向量
        pickle.dump(embedding_matrix, open(dat_fname, 'wb'))  #建立的词嵌入矩阵写回到dat_fname文件中
    return embedding_matrix

class Tokenizer(object):  #字典，包含word2index，index2word功能 参数为最大序列长度，是否小写
    def __init__(self, max_seq_len, lower=True):
        self.lower = lower  #是否小写，由参数给出
        self.max_seq_len = max_seq_len  #最大序列长度，由参数给出

        self.word2idx = {}   #wordd2idx的定义，是一个dict,在创建分词器的时候就通过buildtokenizer填充好了
        self.idx2word = {}   #idx2word定义,idx=index
        self.idx = 1   #index

    def fit_on_text(self, text):  #整个训练集/测试集文件传入，逐个对比建立index索引
        if self.lower:  #如果要求全部小写，默认也是全部小写
            text = text.lower()
        words = text.split()  #以空格为分隔符进行分隔，得到单词列表
        for word in words:  #遍历文本包含的所有单词
            if word not in self.word2idx:  #如果字典不包含当前词
                self.word2idx[word] = self.idx  #新词对应一个新下标
                self.idx2word[self.idx] = word  #该下标对应一个词
                self.idx += 1  #词库的容量

    #example:text_indices = tokenizer.text_to_sequence(text_left + " " + aspect + " " + text_right),只传入一个文本
    def text_to_sequence(self, text, reverse=False, padding='post', truncating='post'):  #将文本转换为字典中的index序列，返回numpy
        '''
        首先根据数据集建立起了{word:index}映射，再根据glove建立起{index:vec}映射
        先将字符串转为index，再截取/填充index，最后再根据需要决定是否反转
        实际传入的时候存在SEP，CLS这种未收录词，就index++，但这个时候对于已经建立好{index:vec}词向量矩阵该如何处理，是否因为使用glove的模型用不上CLS呢？
        :param text:
        :param reverse:
        :param padding:
        :param truncating:
        :return:
        '''
        if self.lower:
            text = text.lower()  #全部小写
        words = text.split()  #去掉空格
        unknownidx = len(self.word2idx)+1  #当前unk单词的index，用于记录当前这句话可能存在的未知
        #写列表生成式时，把要生成的元素放到前面，后面跟for循环，就可以把list创建出来，十分有用，多写几次，很快就可以熟悉这种语法。
        #读取句子中的单词，判断是否在字典中，如果在字典中就通过word2idx得到对应的index，否则就作为unk得到unknownindex加入到index序列中
        sequence = [self.word2idx[w] if w in self.word2idx else unknownidx for w in words]
        #sequence中的index和字典中的index序列一致，这是因为建立分词器的时候文本就是数据集本身
        if len(sequence) == 0:  #如果index序列长度为0，则序列也为0
            sequence = [0]
        if reverse:  #反向生成index序列
            sequence = sequence[::-1]
        return pad_and_truncate(sequence, self.max_seq_len, padding=padding, truncating=truncating)
